package com.douya.common.utils;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;

import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * 说明：entity、mapper、dto、vo的代码生成器，只支持mysql
 *
 * @author 胡晨烨
 * @date 2020/02/01
 */
public class GenerateCodeUtils {

    private static Map<String, Object> templateParams = Maps.newHashMap();

    private static Set<String> doIgnoreColumns = Sets.newHashSet("id", "is_deleted", "create_time", "update_time", "remarks");

    private static Set<String> dtoIgnoreColumns = Sets.newHashSet("is_deleted");

    /**
     * 从数据库直接生成内部服务的JAVA类，包括：VO、DO、Mapper、Service、Feign
     *
     * @param dbUrl            数据库连接url
     * @param dbUser           数据库连接用户名
     * @param dbPassword       数据链接密码
     * @param author           类名生成的作者，填写本人，
     * @param application      服务名称，如：pangu-core-applicaiont，则取中间的"core"
     * @param module           模块名称，如：basic
     * @param tableNamePattern 库表正则，可直接填写库表名称，如：basic_geographic
     * @param basePackage      基础包名，如：com.douya.pangu.core.basic
     * @param isSkipFirstLetter 是否忽略第一个单词：true=是；false=否
     */
    public static void generatorFeign(String dbUrl, String dbUser, String dbPassword, String author, String application,
                                      String module, String tableNamePattern, String basePackage, boolean isSkipFirstLetter) {
        generator(dbUrl, dbUser, dbPassword, author, application, module, tableNamePattern,
                basePackage, true, isSkipFirstLetter);
    }

    /**
     * 从数据库直接生成内部服务的JAVA类，包括：DO、Mapper
     *
     * @param dbUrl             数据库连接url
     * @param dbUser            数据库连接用户名
     * @param dbPassword        数据链接密码
     * @param author            类名生成的作者，填写本人
     * @param application       服务名称，如：pangu-core-applicaiont，则取中间的"core"
     * @param module            模块名称，如：basic
     * @param tableNamePattern  库表正则，可直接填写库表名称，如：basic_geographic
     * @param basePackage       基础包名，如：com.pangu.core.basic
     * @param isSkipFirstLetter 是否忽略第一个单词：true=是；false=否
     */
    public static void generatorEntity(String dbUrl, String dbUser, String dbPassword, String author, String application,
                                       String module, String tableNamePattern, String basePackage, boolean isSkipFirstLetter) {
        generator(dbUrl, dbUser, dbPassword, author, application, module, tableNamePattern, basePackage, true, isSkipFirstLetter);
    }
    
    /**
     * 从数据库直接生成对外服务的JAVA类，包括：VO、DO、Mapper、Service、Controller
     *
     * @param dbUrl             数据库连接url
     * @param dbUser            数据库连接用户名
     * @param dbPassword        数据链接密码
     * @param author            类名生成的作者，填写本人，
     * @param application       服务名称，如：pangu-core-applicaiont，则取中间的"core"
     * @param module            模块名称，如：basic
     * @param tableNamePattern  库表正则，可直接填写库表名称，如：basic_geographic
     * @param basePackage       基础包名，如：com.douya.pangu.core.basic
     * @param isSkipFirstLetter 是否忽略第一个单词：true=是；false=否
     */
    public static void generatorControllerFromDb(String dbUrl, String dbUser, String dbPassword, String author, String application,
                                                 String module, String tableNamePattern, String basePackage, boolean isSkipFirstLetter) {
        generator(dbUrl, dbUser, dbPassword, author, application, module, tableNamePattern, basePackage, false, isSkipFirstLetter);
    }

    /**
     * 从微服务生成对外服务的JAVA类，包括：Service、Controller，暂未实现
     *
     * @param author      类名生成的作者，填写本人，
     * @param remarks     类说明，如：行政区域
     * @param basePackage 基础包名，如：com.douya.pangu.core.basic
     * @param voClass     微服务涉及vo
     * @param dtoClass    微服务涉及的dto
     * @param feignClass  微服务涉及的Feign
     * @param module      模块名称，如：basic
     */
    @SuppressWarnings("rawtypes")
    public static void generatorControllerFromFeign(String author, String remarks, String basePackage,
                                                    Class<?> voClass, Class<?> dtoClass, Class<?> feignClass,
                                                    String module) {
        initTemplateParams(author, remarks, basePackage, voClass, dtoClass, feignClass, module);
        try {
            generateFile(templateParams.get("servicePackage").toString(), "Service", "serviceFromFeign.ftl");
            generateFile(templateParams.get("controllerPackage").toString(), "Controller", "controller.ftl");
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("代码生成完毕");
    }

    public static void generator(String dbUrl, String dbUser, String dbPassword, String author, String application,
                                 String module, String tableNamePattern, String basePackage, boolean isFeign, boolean isSkipFirstLetter) {
        generator(dbUrl, dbUser, dbPassword, author, application, module, tableNamePattern, basePackage, isFeign, false, isSkipFirstLetter);
    }

    public static void generator(String dbUrl, String dbUser, String dbPassword, String author, String application,
                                 String module, String tableNamePattern, String basePackage, boolean isFeign,
                                 boolean isOnlyEntity, boolean isSkipFirstLetter) {
        Connection connection = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            connection = DriverManager.getConnection(dbUrl +
                    "&useInformationSchema=true&user=" + dbUser + "&password=" + dbPassword);
            ResultSet rs = connection.getMetaData()
                    .getTables(null, null, tableNamePattern, null);
            while (rs.next()) {
                String tableName = rs.getString("TABLE_NAME");
                initTemplateParams(author, application, tableName, rs.getString("REMARKS"), basePackage, module, isFeign, isSkipFirstLetter);
                initColumnParams(connection, tableName);
                generateFile(templateParams.get("doPackage").toString(), "DO", "entity.ftl");
                generateFile(templateParams.get("mapperPackage").toString(), "Mapper", "mapper.ftl");
                if (!isOnlyEntity) {
                    generateFile(templateParams.get("voPackage").toString(), "VO", "vo.ftl");
                    generateFile(templateParams.get("dtoPackage").toString(), "DTO", "dto.ftl");
                    generateFile(templateParams.get("servicePackage").toString(), "Service", "service.ftl");
                    if (isFeign) {
                        generateFile(templateParams.get("feignPackage").toString(), "Feign", "feign.ftl");
                        generateFile(templateParams.get("feignImplPackage").toString(), "FeignImpl", "feignImpl.ftl");
                    } else {
                        generateFile(templateParams.get("controllerPackage").toString(), "Controller", "controller.ftl");
                    }
                }
                System.out.printf("生成表：%s%n", tableName);
            }
            rs.close();
        } catch (ClassNotFoundException | SQLException | TemplateException | IOException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("代码生成完毕");
    }

    private static void generateFile(String packageName, String suffix, String templateName)
            throws IOException, TemplateException {
        Template template = loadTemplate(templateName);
        File file = new File(fetchTargetDir(packageName), templateParams.get("class") + suffix + ".java");
        if ((file.getName().contains("Mapper.java") || file.getName().contains("Service.java")) && file.exists()) {
            //Mapper 和 Service 不覆盖
            return;
        }
        OutputStream fos = new FileOutputStream(file);
        Writer out = new OutputStreamWriter(fos);
        template.process(templateParams, out);
        fos.flush();
        fos.close();
        out.close();
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private static File fetchTargetDir(String packageName) {
        // 第一层替换
        String path = Objects.requireNonNull(GenerateCodeUtils.class.getClassLoader().getResource(""))
                .getFile().replaceAll("/target/test-classes/", "/src/main/java/");
        // 环境不同时第二层替换
        path = path.replaceAll("/out/test/classes/", "/src/main/java/");
        final String apiKeyword = ".api.";
        if (packageName.contains(apiKeyword)) {
            path = path.replaceAll("-application", "-api");
        }
        File dir;
        if (isWindows()) {
            path = path.substring(1).replaceAll("/", "\\\\");
            dir = new File(path + packageName.replaceAll("\\.", "\\\\"));
        } else {
            dir = new File(path + packageName.replaceAll("\\.", File.separator));
        }

        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir;
    }

    private static Template loadTemplate(String templateName) throws IOException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
        cfg.setClassLoaderForTemplateLoading(GenerateCodeUtils.class.getClassLoader(),
                "com.douya.common.freemaker.template");
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
        return cfg.getTemplate(templateName);
    }

    @SuppressWarnings("IfCanBeSwitch")
    private static void initColumnParams(Connection connection, String tableName) throws SQLException {
        ResultSet results = connection.getMetaData()
                .getColumns(null, null, tableName, null);
        List<Map<String, Object>> attrs = Lists.newArrayList();
        List<Map<String, Object>> voAttrs = Lists.newArrayList();
        while (results.next()) {
            String columnName = results.getString("COLUMN_NAME");
            String columnTypeName = results.getString("TYPE_NAME");
            String columnRemarks = results.getString("REMARKS");
            String attrName = fetchAttrName(columnName, columnTypeName);
            String attrType = fetchAttrType(columnName, columnTypeName);
            if (!dtoIgnoreColumns.contains(columnName)) {
                if (!doIgnoreColumns.contains(columnName)) {
                    if (Objects.equals("LocalDate", attrType)) {
                        templateParams.put("dtoColumnHasDate", true);
                    } else if (Objects.equals("LocalDateTime", attrType)) {
                        templateParams.put("dtoColumnHasDateTime", true);
                    } else if (Objects.equals("LocalTime", attrType)) {
                        templateParams.put("dtoColumnHasTime", true);
                    }
                }
                Map<String, Object> attr = Maps.newHashMap();
                attr.put("remarks", columnRemarks);
                attr.put("type", attrType);
                attr.put("name", attrName);
                attr.put("nameUpperFirst", toUpperCaseFirstChartter(attrName));
                attr.put("isSetDo", !doIgnoreColumns.contains(columnName));
                voAttrs.add(attr);
                genAttrValidation(attr, results, attrType);
            }
            if (!doIgnoreColumns.contains(columnName)) {
                if (Objects.equals("LocalDate", attrType)) {
                    templateParams.put("doColumnHasDate", true);
                } else if (Objects.equals("LocalDateTime", attrType)) {
                    templateParams.put("doColumnHasDateTime", true);
                } else if (Objects.equals("LocalTime", attrType)) {
                    templateParams.put("doColumnHasTime", true);
                }
                if (Objects.equals("Boolean", attrType)) {
                    templateParams.put("columnHasBol", true);
                }
                Map<String, Object> attr = Maps.newHashMap();
                attr.put("remarks", columnRemarks);
                attr.put("column", columnName);
                attr.put("columnUpperCase", columnName.toUpperCase());
                attr.put("bol", isColumnBoolean(columnName, columnTypeName));
                attr.put("type", attrType);
                attr.put("name", attrName);
                attrs.add(attr);
            }
        }
        results.close();
        templateParams.put("attrs", attrs);
        templateParams.put("dtoAttrs", voAttrs);
    }

    private static void genAttrValidation(Map<String, Object> attr, ResultSet results, String attrType) throws SQLException {
        attr.put("hasMax", false);
        attr.put("hasMin", false);
        attr.put("hasSize", false);
        attr.put("hasNotNull", false);
        String isNullable = results.getString("IS_NULLABLE");
        String size = results.getString("COLUMN_SIZE");
        final String no = "NO", constInteger = "Integer", constLong = "Long", constString = "String";
        if (no.equals(isNullable)) {
            if (constString.equals(attrType)) {
                attr.put("hasNotBlank", true);
                templateParams.put("columnHasNotBlank", true);
            }else {
                attr.put("hasNotNull", true);
                templateParams.put("columnHasNotNull", true);
            }
        }
        if (constInteger.equals(attrType) || constLong.equals(attrType)) {
            attr.put("hasMin", true);
            attr.put("minValue", 0);
            templateParams.put("columnHasMin", true);
        }
        if (constString.equals(attrType)) {
            attr.put("hasSize", true);
            attr.put("sizeMax", Integer.valueOf(size).toString());
            templateParams.put("columnHasSize", true);
        }
    }

    private static String fetchAttrType(String columnName, String columnTypeName) {
        switch (columnTypeName.toUpperCase()) {
            case "BIGINT":
                return "Long";
            case "DATETIME":
            case "TIMESTAMP":
                return "LocalDateTime";
            case "DATE":
                return "LocalDate";
            case "TIME":
                return "LocalTime";
            case "TINYINT":
            case "TINYINT UNSIGNED":
                if (isColumnBoolean(columnName, columnTypeName)) {
                    return "Boolean";
                } else {
                    return "Integer";
                }
            case "INT":
            case "INT UNSIGNED":
                return "Integer";
            default:
                return "String";
        }
    }

    private static String fetchAttrName(String columnName, String columnTypeName) {
        return humpString(isColumnBoolean(columnName, columnTypeName) ? columnName.substring(3) : columnName);
    }

    /**
     * 将下划线分割的字符串转化为驼峰
     */
    private static String humpString(String original) {
        String[] strings = original.split("_");
        StringBuilder sb = new StringBuilder().append(strings[0]);
        for (int i = 1; i < strings.length; i++) {
            sb.append(toUpperCaseFirstChartter(strings[i]));
        }
        return sb.toString();
    }

    /**
     * 首字母大写
     */
    private static String toUpperCaseFirstChartter(String string) {
        return string.substring(0, 1).toUpperCase() + string.substring(1);
    }

    /**
     * 首字母小写
     */
    private static String toLowerCaseFirstChartter(String string) {
        return string.substring(0, 1).toLowerCase() + string.substring(1);
    }

    /**
     * 字段是否是布尔类型
     */
    private static boolean isColumnBoolean(String columnName, String columnTypeName) {
        final String startPrefix = "IS_";
        final String tinyint = "TINYINT";
        return columnTypeName.toUpperCase().contains(tinyint) && columnName.toUpperCase().startsWith(startPrefix);
    }

    /**
     * 是否是windows系统
     */
    private static boolean isWindows() {
        return System.getProperties().getProperty("os.name").toUpperCase().contains("WINDOWS");
    }

    /**
     * 初始化参数-1
     */
    private static void initTemplateParams(String author, String application, String tableName,
                                           String remarks, String basePackage, String module,
                                           boolean isFeign, boolean isSkipFirstLetter) {
        templateParams.clear();
        templateParams.put("author", author);
        templateParams.put("application", application);
        templateParams.put("table", tableName);
        templateParams.put("remarks", remarks);
        String name = humpString(isSkipFirstLetter ? tableName.substring(tableName.indexOf("_") + 1) : tableName);
        String className = toUpperCaseFirstChartter(name);
        templateParams.put("name", name);
        templateParams.put("class", className);
        templateParams.put("columnHasBol", false);
        templateParams.put("dtoColumnHasDate", false);
        templateParams.put("dtoColumnHasDateTime", false);
        templateParams.put("dtoColumnHasTime", false);
        templateParams.put("doColumnHasDate", false);
        templateParams.put("doColumnHasDateTime", false);
        templateParams.put("doColumnHasTime", false);
        templateParams.put("columnHasMax", false);
        templateParams.put("columnHasMin", false);
        templateParams.put("columnHasNotNull", false);
        templateParams.put("columnHasSize", false);
        templateParams.put("doPackage", basePackage + "." + module + ".pojo.entity");
        templateParams.put("mapperPackage", basePackage + "." + module + ".mapper");
        templateParams.put("servicePackage", basePackage + "." + module + ".service");
        templateParams.put("date", DateUtils.format(DateUtils.now(), "yyyy/MM/dd"));
        if (isFeign) {
            templateParams.put("voPackage", basePackage + ".api." + module + ".pojo.vo");
            templateParams.put("dtoPackage", basePackage + ".api." + module + ".pojo.dto");
            templateParams.put("feignPackage", basePackage + ".api." + module + ".feign");
            templateParams.put("feignImplPackage", basePackage + "." + module + ".feign.impl");
            templateParams.put("feignUrl", "api/" + module + "/" + name);
        } else {
            templateParams.put("voPackage", basePackage + "." + module + ".pojo.vo");
            templateParams.put("dtoPackage", basePackage + "." + module + ".pojo.dto");
            templateParams.put("controllerPackage", basePackage + "." + module + ".controller");
            templateParams.put("controllerUrl", "/web/" + module + "/" + name);
        }
    }

    /**
     * 初始化参数-2
     */
    private static void initTemplateParams(String author, String remarks, String basePackage, Class<?> voClass,
                                           Class<?> dtoClass, Class<?> feignClass, String module) {
        templateParams.clear();
        String name = toLowerCaseFirstChartter(voClass.getSimpleName());
        name = name.substring(0, name.length() - 2);
        templateParams.put("author", author);
        templateParams.put("remarks", remarks);
        templateParams.put("class", toUpperCaseFirstChartter(name));
        templateParams.put("name", name);
        templateParams.put("servicePackage", basePackage + "." + module + ".service");
        templateParams.put("controllerPackage", basePackage + "." + module + ".controller");
        templateParams.put("voPackage", voClass.getPackage().getName());
        templateParams.put("dtoPackage", dtoClass.getPackage().getName());
        templateParams.put("feignPackage", feignClass.getPackage().getName());
        templateParams.put("controllerUrl", "c/" + module + "/" + name);
        templateParams.put("date", DateUtils.format(DateUtils.now(), "yyyy/MM/dd"));
    }

}
